Coverage Report

Created: 2026-04-26 08:04

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\csshw\csshw\xtask\src\readme.rs
Line
Count
Source
1
//! README help-section verification and update logic.
2
//!
3
//! The README embeds the `--help` output between two HTML comment delimiters:
4
//!
5
//! ```text
6
//! <!-- HELP_OUTPUT_START -->
7
//! ```cmd
8
//! csshw.exe --help
9
//! <help content>
10
//! ```
11
//! <!-- HELP_OUTPUT_END -->
12
//! ```
13
//!
14
//! [`check_readme_help`] fails when the embedded text differs from the live
15
//! output. [`update_readme_help`] rewrites the README when they differ and
16
//! signals the change to the caller so a pre-commit hook can abort.
17
18
use anyhow::{bail, Context, Result};
19
20
const START_MARKER: &str = "<!-- HELP_OUTPUT_START -->";
21
const END_MARKER: &str = "<!-- HELP_OUTPUT_END -->";
22
const PREAMBLE: &str = "\r\n```cmd\r\ncsshw.exe --help\r\n";
23
const POSTAMBLE: &str = "\r\n```\r\n";
24
25
/// All side-effecting operations required by this module.
26
///
27
/// Implement with mocks in tests to achieve zero filesystem and process
28
/// side-effects.
29
pub trait ReadmeSystem {
30
    /// Run `cargo run --package csshw -- --help` and return the captured output.
31
    ///
32
    /// # Errors
33
    ///
34
    /// Returns an error if the process cannot be started or exits non-zero.
35
    fn get_help_output(&self) -> Result<String>;
36
37
    /// Read the full contents of `README.md`.
38
    ///
39
    /// # Errors
40
    ///
41
    /// Returns an error if the file cannot be read.
42
    fn read_readme(&self) -> Result<String>;
43
44
    /// Write `content` to `README.md`.
45
    ///
46
    /// # Errors
47
    ///
48
    /// Returns an error if the write fails.
49
    fn write_readme(&self, content: &str) -> Result<()>;
50
}
51
52
/// Production implementation of [`ReadmeSystem`].
53
pub struct RealSystem;
54
55
#[cfg_attr(coverage_nightly, coverage(off))]
56
impl ReadmeSystem for RealSystem {
57
    fn get_help_output(&self) -> Result<String> {
58
        let output = std::process::Command::new("cargo")
59
            .args(["run", "--package", "csshw", "--", "--help"])
60
            .output()
61
            .context("failed to run `cargo run --package csshw -- --help`")?;
62
        let raw = String::from_utf8_lossy(&output.stdout).into_owned();
63
        Ok(raw)
64
    }
65
66
    fn read_readme(&self) -> Result<String> {
67
        std::fs::read_to_string("README.md").context("failed to read README.md")
68
    }
69
70
    fn write_readme(&self, content: &str) -> Result<()> {
71
        std::fs::write("README.md", content).context("failed to write README.md")
72
    }
73
}
74
75
/// Normalize raw `--help` output for comparison with the README section.
76
///
77
/// Replaces lines that contain only whitespace with empty lines, normalizes
78
/// all line endings to `\r\n`, and trims leading and trailing whitespace.
79
///
80
/// # Arguments
81
///
82
/// * `raw` - Raw output from `--help`, possibly with mixed line endings.
83
///
84
/// # Returns
85
///
86
/// Normalized string ready for comparison with the README section.
87
///
88
6
pub fn normalize_help_output(raw: &str) -> String {
89
6
    let normalized: Vec<&str> = raw
90
6
        .lines()
91
9
        .
map6
(|line| if line.trim().is_empty() {
""1
} else {
line8
})
92
6
        .collect();
93
6
    let joined = normalized.join("\r\n");
94
6
    joined.trim().to_owned()
95
6
}
96
97
/// Extract the help text embedded in the README between the delimiters.
98
///
99
/// # Arguments
100
///
101
/// * `readme` - Full README contents.
102
///
103
/// # Returns
104
///
105
/// The help content string (trimmed), or an error if either delimiter is missing.
106
///
107
/// # Errors
108
///
109
/// Returns an error if `<!-- HELP_OUTPUT_START -->` or `<!-- HELP_OUTPUT_END -->`
110
/// is absent, or if the expected preamble/postamble structure is not found.
111
7
pub fn extract_readme_help_section(readme: &str) -> Result<&str> {
112
7
    let 
start_marker_pos6
= readme
113
7
        .find(START_MARKER)
114
7
        .context("could not find <!-- HELP_OUTPUT_START --> in README.md")
?1
;
115
6
    let 
end_marker_pos5
= readme
116
6
        .find(END_MARKER)
117
6
        .context("could not find <!-- HELP_OUTPUT_END --> in README.md")
?1
;
118
119
5
    let content_start = start_marker_pos + START_MARKER.len() + PREAMBLE.len();
120
5
    let content_end = end_marker_pos - POSTAMBLE.len();
121
122
5
    if content_start > content_end {
123
0
        bail!("README help section delimiters are malformed or out of order");
124
5
    }
125
126
5
    Ok(readme[content_start..content_end].trim())
127
7
}
128
129
/// Rebuild the README with the help section replaced by `new_help`.
130
///
131
/// All content outside the delimiters and the fixed preamble/postamble is
132
/// preserved byte-for-byte.
133
///
134
/// # Arguments
135
///
136
/// * `readme` - Full README contents.
137
/// * `new_help` - Normalized help text to embed.
138
///
139
/// # Returns
140
///
141
/// New full README string.
142
///
143
/// # Errors
144
///
145
/// Returns an error if the delimiters are not found.
146
2
pub fn replace_readme_help_section(readme: &str, new_help: &str) -> Result<String> {
147
2
    let start_marker_pos = readme
148
2
        .find(START_MARKER)
149
2
        .context("could not find <!-- HELP_OUTPUT_START --> in README.md")
?0
;
150
2
    let end_marker_pos = readme
151
2
        .find(END_MARKER)
152
2
        .context("could not find <!-- HELP_OUTPUT_END --> in README.md")
?0
;
153
154
2
    let content_start = start_marker_pos + START_MARKER.len() + PREAMBLE.len();
155
2
    let content_end = end_marker_pos - POSTAMBLE.len();
156
157
2
    let before = &readme[..content_start];
158
2
    let after = &readme[content_end..];
159
160
2
    Ok(format!("{before}{new_help}{after}"))
161
2
}
162
163
/// Compare the live `--help` output against the README's embedded help section.
164
///
165
/// Prints a colored diff to stdout when they differ.
166
///
167
/// # Arguments
168
///
169
/// * `system` - Injected I/O provider.
170
///
171
/// # Returns
172
///
173
/// `Ok(())` if they match; an error describing the mismatch otherwise.
174
///
175
/// # Errors
176
///
177
/// Returns an error when the sections differ or when any I/O operation fails.
178
2
pub fn check_readme_help<S: ReadmeSystem>(system: &S) -> Result<()> {
179
2
    let raw_help = system.get_help_output()
?0
;
180
2
    let actual_help = normalize_help_output(&raw_help);
181
182
2
    let readme = system.read_readme()
?0
;
183
2
    let readme_help = extract_readme_help_section(&readme)
?0
;
184
185
2
    if actual_help == readme_help {
186
1
        println!("INFO - README.md help output is up to date.");
187
1
        return Ok(());
188
1
    }
189
190
1
    eprintln!("ERROR - README.md help output is outdated!");
191
1
    eprintln!();
192
1
    eprintln!("Differences found:");
193
1
    eprintln!("==================");
194
1
    eprintln!("README has:");
195
1
    eprintln!("{readme_help}");
196
1
    eprintln!();
197
1
    eprintln!("Current --help output:");
198
1
    eprintln!("{actual_help}");
199
1
    eprintln!();
200
1
    eprintln!("==> Run `cargo xtask update-readme-help` to fix this.");
201
202
1
    bail!("README.md help output is outdated")
203
2
}
204
205
/// Ensure the README's embedded help section matches the live `--help` output,
206
/// writing an updated README when they differ.
207
///
208
/// # Arguments
209
///
210
/// * `system` - Injected I/O provider.
211
///
212
/// # Returns
213
///
214
/// `Ok(true)` when the README was modified (the caller should exit with code 1
215
/// to abort a pre-commit hook); `Ok(false)` when already up to date.
216
///
217
/// # Errors
218
///
219
/// Returns an error when any I/O operation fails.
220
2
pub fn update_readme_help<S: ReadmeSystem>(system: &S) -> Result<bool> {
221
2
    let raw_help = system.get_help_output()
?0
;
222
2
    let actual_help = normalize_help_output(&raw_help);
223
224
2
    let readme = system.read_readme()
?0
;
225
2
    let readme_help = extract_readme_help_section(&readme)
?0
;
226
227
2
    if actual_help == readme_help {
228
1
        println!("INFO - README.md help section is up to date, nothing to be done.");
229
1
        return Ok(false);
230
1
    }
231
232
1
    println!("WARNING - README.md help section is outdated — fixing it.");
233
1
    let new_readme = replace_readme_help_section(&readme, &actual_help)
?0
;
234
1
    system.write_readme(&new_readme)
?0
;
235
1
    println!("INFO - README.md help section has been updated with current --help output.");
236
237
1
    Ok(true)
238
2
}
239
240
#[cfg(test)]
241
#[path = "tests/test_readme.rs"]
242
mod tests;